agentsclimarketplace

Adding azure communication services email

Skill alexpizarro/azure-lean-stack-skills/skills/adding-azure-communication-services-email

Adds transactional email to an Azure web app via Azure Communication Services (ACS) Email — verification emails, password resets, notifications. Encodes the three ACS quirks that consistently break Bicep deploys (location:'global' literal, dataLocation in plain English not Azure region IDs, declare-order to avoid circular dependency), and the safeSend() wrapper that prevents email failures from crashing HTTP handlers. Use when adding email sending to a project, fixing ACS Bicep deployment errors, or migrating off SendGrid/Mailgun.From its SKILL.md

Install
npx -y skills add alexpizarro/azure-lean-stack-skills --skill adding-azure-communication-services-email

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

3 things to look at

  • reads credentialsReads from 1 credential source: `process.env.ACS_CONNECTION_STRING`.
  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
  • runs commandsInstructs the agent to run 2 commands, including `az communication email domain show --resource-group "$RG" --email-service-name "$EMAIL_SERVICE_NAME" --name AzureManagedDomain --query "fromSenderDomain" -o tsv` and 1 more.

SKILL.md

6.6 KB, ~1.6k tokens by cl100k_base, as published. Nobody here has run it

Adding Azure Communication Services Email

Transactional email via Azure Communication Services. Free tier: 100 emails/day, then $0.00025/email. Cheaper than SendGrid for low-volume projects.

When to use ACS Email

NeedUse
Account verification, password reset, transactional notificationsACS Email (this skill)
Marketing campaigns, newsletters, A/B testingA marketing-focused provider (Mailchimp, etc.) — ACS doesn't do that
High-volume bulk send (>100k/day)SendGrid Pro or Mailgun (ACS gets expensive at scale)

The three ACS quirks

1. location: 'global' (literal string, not a real Azure region)

resource emailService 'Microsoft.Communication/emailServices@2023-04-01' = {
  name: emailServiceName
  location: 'global'                  // ← NOT 'australiaeast', NOT location param
  properties: { dataLocation: 'Australia' }
}

If you pass the project's location param (australiaeast), Azure rejects the deploy.

2. dataLocation uses plain English

The values for dataLocation are not Azure region IDs. They're region groupings:

dataLocation valueWhere data is stored
'Australia'AU data centres
'Europe'EU
'United States'US
'Asia Pacific'APAC excluding AU
'Africa'Africa
'Brazil'Brazil
'Canada'Canada
'France'France
'Germany'Germany
'India'India
'Japan'Japan
'Korea'Korea
'Norway'Norway
'Switzerland'Switzerland
'UAE'UAE
'United Kingdom'UK

Use 'Australia' — not 'australiaeast', not 'au', not 'aus'.

3. Declare-order — no dependsOn on email service + domain

ACS resources have an awkward circular dependency: the ACS resource needs to know about the domain, but linkedDomains + dependsOn causes deployment failures.

Declare in this order, with no dependsOn:

// 1. Email service (no dependencies)
resource emailService 'Microsoft.Communication/emailServices@2023-04-01' = {
  name: emailServiceName
  location: 'global'
  properties: { dataLocation: 'Australia' }
}

// 2. Domain — child of emailService
resource emailDomain 'Microsoft.Communication/emailServices/domains@2023-04-01' = {
  parent: emailService
  name: 'AzureManagedDomain'        // or your custom domain
  location: 'global'
  properties: {
    domainManagement: 'AzureManaged'    // or 'CustomerManaged'
    userEngagementTracking: 'Disabled'
  }
}

// 3. Comms service — links to the domain via linkedDomains
resource acs 'Microsoft.Communication/communicationServices@2023-04-01' = {
  name: acsName
  location: 'global'
  properties: {
    dataLocation: 'Australia'
    linkedDomains: [ emailDomain.id ]
  }
}

No dependsOn blocks anywhere — the implicit dependency through parent: and linkedDomains: is enough.

The Azure-managed domain has an unknown name

When using AzureManagedDomain, the actual sending address looks like:

DoNotReply@<random-hash>.azurecomm.net

You can't predict the hash before deployment. Retrieve it post-deploy:

az communication email domain show \
  --resource-group "$RG" \
  --email-service-name "$EMAIL_SERVICE_NAME" \
  --name AzureManagedDomain \
  --query "fromSenderDomain" -o tsv

Set EMAIL_FROM as a SWA / Function App setting after the first deploy.

The safeSend() wrapper

@azure/communication-email uses an async poller — beginSend() returns immediately, you call pollUntilDone(). If pollUntilDone() throws (network error, quota, etc.), your HTTP handler crashes with a 500.

Wrap it:

import { EmailClient } from '@azure/communication-email';

const client = new EmailClient(process.env.ACS_CONNECTION_STRING!);

export async function safeSend(message: {
  senderAddress: string;
  recipients: { to: { address: string }[] };
  content: { subject: string; plainText?: string; html?: string };
}): Promise<{ ok: true; messageId: string } | { ok: false; error: string }> {
  try {
    const poller = await client.beginSend(message);
    const result = await poller.pollUntilDone();
    if (result.status === 'Succeeded') {
      return { ok: true, messageId: result.id };
    }
    return { ok: false, error: result.error?.message ?? `status=${result.status}` };
  } catch (err: any) {
    // Log but never throw — email failure must not crash the HTTP handler
    console.error('Email send failed:', err);
    return { ok: false, error: err.message ?? String(err) };
  }
}

Then call from your handler:

const result = await safeSend({
  senderAddress: process.env.EMAIL_FROM!,
  recipients: { to: [{ address: user.email }] },
  content: {
    subject: 'Verify your email',
    html: renderVerifyEmail(token),
  },
});

if (!result.ok) {
  ctx.warn(`Email send failed for ${user.email}: ${result.error}`);
  // Continue — perhaps queue for retry, but don't fail the user's signup
}

Custom domains

To send from [email protected], set domainManagement: 'CustomerManaged'. Then:

  1. ACS gives you DNS records (SPF, DKIM, DMARC) to add
  2. After DNS propagates, run az communication email domain initiate-verification
  3. Use the custom sender address in senderAddress

The Azure-managed domain is fine for prototypes and internal tools.

Pricing

  • 100 emails/day free
  • $0.00025/email above that
  • Attachments billed extra at $0.0002/MB

For a 1000-user app doing ~5 transactional emails/user/month (5k/month), cost is ~$1.25/month.

Composes with

Templates

FilePurpose
templates/acs.bicepEmail service + Azure-managed domain + ACS resource

What ships with it: 1 file

2.1 KB alongside SKILL.md

templates/

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.