Skip to main content
Human surveys support email delivery to respondents, scheduled send campaigns, and event-triggered callbacks. All of these are managed through HumanSurveyNotificationHandler, a convenience wrapper that binds to a specific human survey so you don’t have to pass the UUID on every call.
The survey must have an agent list with an email delivery channel configured before any notifications can be sent. Set this up when calling survey.humanize() by passing agent_list and delivery_map arguments.

Getting a handler

from edsl.coop import HumanSurveyNotificationHandler

handler = HumanSurveyNotificationHandler("your-human-survey-uuid")
A Coop client is created automatically. Pass one explicitly if you need a specific API key or base URL:
from edsl import Coop
from edsl.coop import HumanSurveyNotificationHandler

coop = Coop()
handler = HumanSurveyNotificationHandler("your-human-survey-uuid", coop=coop)

Statuses

Every delivery task moves through two independent status tracks.

Dispatch status

Controls whether the platform has attempted to hand off the message to the downstream channel (e.g. the email provider).
ValueMeaning
pendingThe task has been created but not yet processed.
dispatchedThe message was successfully handed off to the channel.
failedThe dispatch attempt failed (e.g. missing address, provider error).

Delivery status

Reflects what the email provider reports back after dispatch.
ValueMeaning
pendingWaiting for a delivery event from the provider.
sentThe provider accepted the message for delivery.
deliveredThe provider confirmed the message reached the recipient.
bouncedThe message was rejected by the recipient’s mail server.
failedThe provider reported an error sending the email.

Response status

Tracks whether a respondent has engaged with the survey itself.
ValueMeaning
not_startedThe respondent has not opened the survey link.
completedThe respondent submitted a complete response.

Quickstart examples

Send an email right now

Trigger an immediate delivery to all respondents.
result = handler.send_respondent_email(name="Initial outreach")
print(result)
# {"delivery_uuid": "...", "routes": [...]}
Pass a custom subject, HTML email body, or restrict which respondents receive it:
from edsl.coop.coop_humanize_notifications import (
    HumanizeRespondentFilter,
    RespondentCondition,
    ResponseStatus,
)

# Only email respondents who haven't started yet
condition = RespondentCondition(
    response_status=[ResponseStatus.not_started]
)
respondent_filter = HumanizeRespondentFilter(conditions=[condition])

result = handler.send_respondent_email(
    name="Reminder to non-starters",
    subject="Don't forget to complete our survey",
    delivery_template="<p>Hi! Just a reminder to complete our survey.</p>",
    respondent_filter=respondent_filter,
)

Schedule a one-time email

Schedule a single delivery at a specific time. run_at must be a timezone-aware datetime or an ISO 8601 string.
from datetime import datetime, timezone

run_at = datetime(2026, 6, 1, 9, 0, tzinfo=timezone.utc)

schedule = handler.create_one_time_schedule(
    name="June 1st reminder",
    run_at=run_at,
)
print(schedule["schedule_uuid"])
Update the time before it fires:
handler.update_one_time_schedule(
    schedule_uuid=schedule["schedule_uuid"],
    run_at="2026-06-02T09:00:00+00:00",
)

Schedule recurring emails (cron)

Send emails on a repeating schedule using standard cron syntax. Provide either max_jobs (fire at most N times) or deadline (stop firing after this datetime) as the termination condition. Use start_at to delay when the schedule begins firing; omit it to start immediately.
from datetime import datetime, timezone

# Every Monday at 9 AM Eastern, up to 4 times
schedule = handler.create_cron_schedule(
    name="Weekly Monday reminder",
    cron_expression="0 9 * * MON",
    timezone="America/New_York",
    max_jobs=4,
)
With a deadline instead:
schedule = handler.create_cron_schedule(
    name="Daily reminders through end of month",
    cron_expression="0 8 * * *",
    timezone="America/Chicago",
    deadline=datetime(2026, 6, 30, 23, 59, tzinfo=timezone.utc),
)
With a delayed start:
schedule = handler.create_cron_schedule(
    name="Weekly reminders starting next month",
    cron_expression="0 9 * * MON",
    timezone="America/New_York",
    max_jobs=4,
    start_at=datetime(2026, 6, 1, tzinfo=timezone.utc),
)
Pause and resume a schedule:
handler.deactivate_schedule(schedule["schedule_uuid"])
handler.activate_schedule(schedule["schedule_uuid"])

React when someone completes the survey (callback)

A respondent is a person with a corresponding entry in the survey’s agent list. Each respondent has a unique survey link and a tracked response status. Submissions from people outside the agent list (e.g. anonymous links) are not associated with a respondent. create_callback sends emails to respondents in your agent list. Two respondent event types are supported:
EventWhen it fires
"human_survey_respondent.completed"When a respondent’s status is set to completed.
"human_survey_respondent.response_submitted"Each time a respondent submits a response.
callback = handler.create_callback(
    name="Completion thank-you",
    callback_type="human_survey_respondent.completed",
)
print(callback["callback_uuid"])
Limit how many times the callback fires (e.g. only notify for the first 100 completions):
callback = handler.create_callback(
    name="First 100 completions",
    callback_type="human_survey_respondent.completed",
    max_fires=100,
)

Notify the owner on every new response

Use create_owner_response_callback to email the survey owner each time a response is submitted. This fires for all responses, including anonymous ones — not just respondents in the agent list.
callback = handler.create_owner_response_callback(name="New response notification")
print(callback["callback_uuid"])

Send a transcript after each submission

Use create_transcript_callback to automatically email a transcript each time a response is submitted. Pass recipient to control who receives it:
  • "respondent" (default) — emails each respondent a copy of their own answers.
  • "owner" — emails the survey owner a copy of the answers. Fires for all submissions, including anonymous ones.
# Email the respondent their own transcript
callback = handler.create_transcript_callback(
    name="Send respondent transcript on submission",
    recipient="respondent",
)

# Email the owner a transcript on every submission
callback = handler.create_transcript_callback(
    name="Send owner transcript on submission",
    recipient="owner",
)

print(callback["callback_uuid"])
Activate and deactivate:
handler.deactivate_callback(callback["callback_uuid"])
handler.activate_callback(callback["callback_uuid"])

Filter which respondents receive a delivery

HumanizeRespondentFilter lets you compose conditions to target specific subsets of respondents. Conditions can be nested with and / or operators.
from edsl.coop.coop_humanize_notifications import (
    HumanizeRespondentFilter,
    RespondentCondition,
    DeliveryStatus,
    DispatchStatus,
    ResponseStatus,
    FilterOperator,
)

# Target respondents who were never contacted
never_contacted = RespondentCondition(never_contacted=True)

# OR respondents whose last email bounced
bounced = RespondentCondition(
    most_recent_delivery_status=[DeliveryStatus.bounced]
)

# Combine: never contacted OR previously bounced
respondent_filter = HumanizeRespondentFilter(
    operator=FilterOperator.or_,
    conditions=[never_contacted, bounced],
)

handler.send_respondent_email(
    name="Re-engagement",
    respondent_filter=respondent_filter,
)
Available condition fields:
FieldTypeFilters to respondents where…
respondent_uuidslist[str]UUID is in the provided list
response_statuslist[ResponseStatus]Survey response status matches
never_contactedboolHas never received a delivery
any_dispatch_statuslist[DispatchStatus]Any past task has this dispatch status
any_delivery_statuslist[DeliveryStatus]Any past task has this delivery status
most_recent_dispatch_statuslist[DispatchStatus]Most recent task has this dispatch status
most_recent_delivery_statuslist[DeliveryStatus]Most recent task has this delivery status

Check delivery status

List all delivery jobs for the survey:
deliveries = handler.list_deliveries()
for d in deliveries["deliveries"]:
    print(d["delivery_uuid"], d["status"], d["sent_count"], d["failed_count"])
Inspect a specific delivery job:
delivery = handler.get_delivery(delivery_uuid="...")
print(delivery)
# {
#   "delivery_uuid": "...",
#   "status": "completed",
#   "total_respondents": 50,
#   "processed_respondents": 50,
#   "sent_count": 48,
#   "failed_count": 2,
#   "started_at": "...",
#   "completed_at": "..."
# }
Drill into individual tasks (one per respondent per delivery):
tasks = handler.list_delivery_tasks(delivery_uuid="...")
for task in tasks["tasks"]:
    print(task["identifier"], task["dispatch_status"], task["delivery_status"])
Get a single delivery task by UUID:
task = handler.get_delivery_task(task_uuid="...")
print(task)
# {
#   "task_uuid": "...",
#   "channel": "email",
#   "identifier": "[email protected]",
#   "dispatch_status": "dispatched",
#   "delivery_status": "delivered",
#   "respondent": {"respondent_uuid": "...", "response_status": "completed"},
#   ...
# }

Route types

Each delivery or schedule sends email to survey respondents via a RespondentEmailRouteConfig route. A delivery template is always required; convenience methods like send_respondent_email default to the built-in invitation template when none is provided. RespondentEmailRouteConfig options:
FieldDescription
delivery_templateThe email body. Pass an HTML string to use a custom template, or omit to use the built-in invitation template.
respondent_filterHumanizeRespondentFilter to restrict which respondents receive the email.
subjectEmail subject line (1-200 characters). Uses the default subject when omitted.

Template variables

Custom HTML templates support the following variables, which are filled in per-respondent at send time:
VariableValue
{{ survey_name }}The name of the human survey.
{{ url }}The respondent’s unique survey link (includes their access token).
Example:
handler.send_respondent_email(
    name="Initial outreach",
    subject="You're invited to take our survey",
    delivery_template="""
<p>Hi there,</p>
<p>You've been invited to take <strong>{{ survey_name }}</strong>.</p>
<p><a href="{{ url }}">Click here to begin</a></p>
""",
)
Update the subject and template on an existing respondent email route on a schedule:
handler.patch_schedule_respondent_email_route(
    schedule_uuid="...",
    route_uuid="...",
    subject="A quick reminder about our survey",
    delivery_template="<p>Hi! Please take a moment to complete our survey.</p>",
)
Update the subject and template on an existing respondent email route on a callback:
handler.patch_callback_respondent_email_route(
    callback_uuid="...",
    route_uuid="...",
    subject="Thanks for completing our survey",
    delivery_template="<p>Hi! Thank you for completing <strong>{{ survey_name }}</strong>.</p>",
)

Managing respondents

List all respondents and their current response status:
respondents = handler.get_respondents()
for r in respondents["respondents"]:
    print(r["respondent_uuid"], r["response_status"])
Check and update the agent list delivery configuration (the trait column that holds each respondent’s email address):
from edsl.coop.coop_humanize_notifications import ChannelConfig, DeliveryMap

# See current config
config = handler.get_agent_list()
print(config["agent_list_config"])

# Update the email column mapping
handler.patch_agent_list(
    delivery_map=DeliveryMap(email=ChannelConfig(col_name="email_address"))
)