Announcing our $28M Series A. Read More

The Modern Marketing Automation Platform

Conversion is the AI-native Marketing Automation Platform built for high-growth B2B businesses. Activate customer data, build custom journeys, and send beautiful emails to every single customer.

Powering the world’s best product teams,
From next-gen startups to established enterprises.

Workflows

Build custom workflows that adapt to every customer

Trigger emails, update CRM records, and assign leads based on product usage, enrichment data, and more — all without writing code.

Learn more
Image of the workflows editor in the conversion app

Design beautiful emails with drag and drop editing

Build high-performing emails in minutes. Save blocks, add dynamic content, and stay on-brand without any developer help.

Learn more
image of the emails designer in the conversion app

Manage your leads with CRM integrations and enrichment

Keep everything connected. Conversion syncs seamlessly with your CRM, product events, enrichment, billing tools, and more.

Learn more
image of the leads crm in the conversion app

Take the guesswork out of campaign performance

Track what’s working and what isn’t in real time. Use Conversion’s native reporting to see performance by segment, channel, and touchpoint.

Learn more
image of the campaign analytics dashboard in the conversion app

The Case for Conversion

Legacy marketing platforms are stuck in the 2000s. Conversion is built for speed and personalization. Here’s why modern B2B teams are making the switch.

image of the campaign builder within the conversion app
image showing price per usage in the conversion app
Speed to launch

Build and ship campaigns in hours, not weeks, with modern tools and flexible workflows.

image of the ai in the conversion app tailoring emails baed on personas
image showing price per usage in the conversion app
Hyper personalization

Automatically segment and tailor emails based on personas with the power of AI.

image of a placeholder email builder
image showing price per usage in the conversion app
Design-first email builder

Create beautiful, responsive, on-brand emails without a line of code.

image of the syncing of campaigns and leads in the conversion app
image showing price per usage in the conversion app
Native CRM syncing

Build and ship campaigns in hours, not weeks, with modern tools and flexible workflows.

graphic showing the layers of use cases in the conversion app
image showing price per usage in the conversion app
All-in-one platform

Unify email, segmentation, scoring, workflows, and reporting.

image showing price per usage in the conversion app
Pay for what you use

Only pay for the contacts you actively market to. No inflated pricing. No hidden limits.

CUSTOMERS

Trusted by the best

Video preview

“With Conversion, we built a campaign we’re genuinely proud of. Fast to launch, easy to maintain, and better performing than anything we ran before.”

Read Case Study
CUSTOMERS

Trusted by the best

Video preview
image of a person providing a testimonial

"Conversion plays a key role in how we run persona-based campaigns at scale. We use it to enrich, segment, and personalize every touchpoint more efficiently than we ever could with Hubspot. We use it to enrich, segment, and personalize every touchpoint more efficiently than we ever could with Hubspot."

Read Case Study
Video preview
image of a person providing a testimonial

“With Conversion, we built a campaign we’re genuinely proud of. Fast to launch, easy to maintain, and better performing than anything we ran before.”

Read Case Study
Video preview
image of a person providing a testimonial

"Conversion plays a key role in how we run persona-based campaigns at scale."

Read Case Study

Templates for absolutely anything

From lifecycle emails to outbound nurture to product launches.
 Browse our full library of plug-and-play templates.

Built for modern GTM teams

Create faster. Stay aligned. Scale with confidence. Conversion gives every team the power to automate, personalize, and grow.

preview images of the conversion app for enterprise customers
Enterprise compliance

From granular permissions to robust audit trails, we give modern B2B companies the control they need.

image showing custom data models
Custom data models

Build automations that fit your GTM motion — from free trials to usage-based billing to sales-led onboarding.

image showing a search bar in which you can search for multiple dynamic filters within the conversion app
Rich context

Surface insights faster with powerful search, dynamic filters, and rich event history. Everything stays accessible and actionable.

integrations

One-Click Integrations. Infinite Potential.

Conversion integrates seamlessly with your CRM, data warehouse, CDP, ad tools, and analytics to turn siloed data into coordinated, AI-powered campaigns.

Book demo
image showing the various integrations that conversion has

Relevant resources

Button Text

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Engineering

Workflows At Scale: How Conversion Powers Marketing Automation

October 9, 2025
00 Minute Read

Workflows are the new chat. Workflows are fundamentally replacing the way enterprise teams interact with AI and unlock advanced reasoning. At Conversion, we are building the infrastructure for marketers to easily build and run these workflows at massive scale – millions of contacts, hundreds of automations with no room for error. It’s a hard problem with deep technical challenges, and we want to share how we’re tackling it.

This feature has three main components:

  1. The UI Workflow builder
  2. Workflow Triggering
  3. Workflow Execution

The UI builder and workflow triggering deserve their own blog, so we will only focus on the 3rd component - actually running a workflow for a contact.

The Problem Statement

Each Conversion workflow that you see in the UI is stored as a list of nodes and edges, so executing the workflow is as simple as moving through the DAG, executing nodes.

func RunAutomation(ctx workflow.Context, nodeId, contactId string) (err error){
	for {
    	// Fetch the node
        node := fetchNode(nodeId)‍
        
        // Execute the node
        nodeId, err := ExecuteNode(ctx, input.ContactId, node)
        }
}

What makes executing the nodes challenging is three functional requirements:

  1. Users can interact with running workflows: pausing and updating the configuration of each node.
  2. The “wait” node requires that this execution has to be able to run for weeks or even months.
  3. The “wait for” node requires that this execution can wait forever, and continually check a condition, only continuing when the condition is met.

Your first instinct might be a job system – using a cron job and some microservices to maintain state while kubernetes or airflow jobs run. But in reality any job-based solution is already ruled out due to the first requirement. 

With short lived jobs we are okay with the code used to run the job being pulled at the beginning, basically taking a snapshot of the binary. However, with long lived jobs exposed to users like this, we cannot limit the workflow builder to only support running code that was present when the execution first started. We found an underlying devops requirement: the jobs underlying code needs to be able to change while it is running.

Introducing: Temporal

Luckily, this problem has already been tackled by some of the biggest tech companies out there – and most recently, by Temporal, an open-source workflow engine.

The key idea behind Temporal is to abstract away the concept of a long-lived job so that you can change the code underneath without breaking ongoing work. Temporal does this by breaking a long job into a series of smaller, independent steps called “activities” (things like API calls, database writes, etc.) and separating them from the surrounding workflow logic that decides what happens next.

Here’s the magic part: Temporal records the results of each activity. So, if your workflow is paused — maybe waiting days, weeks, or even months — you can “resume” it simply by replaying the logic. Temporal re-runs your workflow code from the beginning, but instead of actually calling the APIs again, it just returns the same results from before.

Imagine you have a workflow that runs Activity A, then B, then C, and then waits five days. When you come back later, Temporal replays A → B → C instantly using the stored results, skips the waiting period, and picks up right where it left off.

This makes Temporal agnostic to the underlying binary — it doesn’t care about your code version as long as the timeline of inputs and outputs stays consistent. As long as your workflow is deterministic (same inputs → same outputs → same decisions), the replay will be identical. This is crucial for our devops requirement that a cron job simply couldn’t solve for.

Temporal also comes pre-built with a signaling system, allowing us to interact with running workflows really easily. We can signal all running temporal workflows that the user wishes to pause execution, or that the user updated a node's data. This solves our first functional requirement. 

Temporal timers solve for the second functional requirement as they allow us to sleep for an arbitrary amount of time, replaying that event history we talked about when the timer goes off. We had to wrap this using the signal system for a pausable timer. This interface is shown below where our timer can handle UI state changes.

type PausableTimer[T NodeData] interface {
	Update(ctx workflow.Context, node TypedNode[T])
	Pause(ctx workflow.Context)
	Resume(ctx workflow.Context)
	IsComplete() bool
	Complete(ctx workflow.Context)
	Listen(ctx workflow.Context, selector workflow.Selector)
}

We can now handle when the user updates the node, pauses the workflow, resumes etc. for different timer nodes. So now users and devs can change how this node works in real time.

A final functional requirement also leverages the signaling system. Workflows can include logic such as “wait until a contact’s record is updated,” and must resume once that change occurs. To support this, we’ve implemented an event notification mechanism that alerts all relevant workflows whenever an activity takes place. When notified, each workflow wakes up, evaluates whether its continuation condition is met, and, if not, returns to a waiting state until it is.

Hosting

Temporal is written in go with a nice SDK that plays nice with our backend. We host our own instance on a postgres database, which has been super easy to manage with the open source helm chart and has given us exactly the kind of scale we require. 

There are tons of limits and guardrails hidden all over Temporal but after finding these gotchas we have been very happy with this service in general. We deployed elastic search with custom search attributes for easy debugging and exposed the UI behind google authentication for easy access by our backend team. A key decision we made here was deploying a different worker for each task queue, so that if a task queue gets bogged down, other workers will continue to process. 

Drawbacks and Challenges

Temporal (and similar workflow engines) solves one of the hardest problems in distributed systems,  reliably managing long-lived processes, but it comes with its own set of challenges. The biggest hurdle is enforcing backward compatibility across workflow versions. A single non-deterministic code change or incompatible deployment could break every running workflow in production. We’ve built a rigorous history replay testing system to constantly sample and replay current histories to catch these issues early, but this area will demand even more infrastructure investment as we scale.

Another pain point is Temporal’s signal system, which isn’t designed for the high-throughput signaling we need. Signal rate limits make large-scale fan-out difficult, and the batch signal API proved too slow and limited in parallelism. To work around this, we use Kafka to stream signals efficiently, both for workflow triggers and CRM event propagation, while caching workflow configurations to determine when a signal actually needs to be sent. This ensures we stay accurate without overloading the system.

Looking ahead, we’ll face new scaling challenges: managing more task queues, namespaces, and workers will require full infrastructure-as-code automation. We’ll also need reliable recovery systems for failed workflows, and worker versioning to reduce risk during large updates. Most of our future optimization will happen at the event-sending layer: making signaling faster, more selective, and more reliable so that workflows start and progress exactly when and how they should.

If any of these challenges sound interesting to you, Conversion is hiring! We’re constantly working to build new features, improve reliability, and scalability. Please reach out if you’d like to join us on the mission of building the future of marketing.

Engineering

Workflows At Scale: How Conversion Powers Marketing Automation

October 9, 2025
00 Minute Read

Workflows are the new chat. Workflows are fundamentally replacing the way enterprise teams interact with AI and unlock advanced reasoning. At Conversion, we are building the infrastructure for marketers to easily build and run these workflows at massive scale – millions of contacts, hundreds of automations with no room for error. It’s a hard problem with deep technical challenges, and we want to share how we’re tackling it.

This feature has three main components:

  1. The UI Workflow builder
  2. Workflow Triggering
  3. Workflow Execution

The UI builder and workflow triggering deserve their own blog, so we will only focus on the 3rd component - actually running a workflow for a contact.

The Problem Statement

Each Conversion workflow that you see in the UI is stored as a list of nodes and edges, so executing the workflow is as simple as moving through the DAG, executing nodes.

func RunAutomation(ctx workflow.Context, nodeId, contactId string) (err error){
	for {
    	// Fetch the node
        node := fetchNode(nodeId)‍
        
        // Execute the node
        nodeId, err := ExecuteNode(ctx, input.ContactId, node)
        }
}

What makes executing the nodes challenging is three functional requirements:

  1. Users can interact with running workflows: pausing and updating the configuration of each node.
  2. The “wait” node requires that this execution has to be able to run for weeks or even months.
  3. The “wait for” node requires that this execution can wait forever, and continually check a condition, only continuing when the condition is met.

Your first instinct might be a job system – using a cron job and some microservices to maintain state while kubernetes or airflow jobs run. But in reality any job-based solution is already ruled out due to the first requirement. 

With short lived jobs we are okay with the code used to run the job being pulled at the beginning, basically taking a snapshot of the binary. However, with long lived jobs exposed to users like this, we cannot limit the workflow builder to only support running code that was present when the execution first started. We found an underlying devops requirement: the jobs underlying code needs to be able to change while it is running.

Introducing: Temporal

Luckily, this problem has already been tackled by some of the biggest tech companies out there – and most recently, by Temporal, an open-source workflow engine.

The key idea behind Temporal is to abstract away the concept of a long-lived job so that you can change the code underneath without breaking ongoing work. Temporal does this by breaking a long job into a series of smaller, independent steps called “activities” (things like API calls, database writes, etc.) and separating them from the surrounding workflow logic that decides what happens next.

Here’s the magic part: Temporal records the results of each activity. So, if your workflow is paused — maybe waiting days, weeks, or even months — you can “resume” it simply by replaying the logic. Temporal re-runs your workflow code from the beginning, but instead of actually calling the APIs again, it just returns the same results from before.

Imagine you have a workflow that runs Activity A, then B, then C, and then waits five days. When you come back later, Temporal replays A → B → C instantly using the stored results, skips the waiting period, and picks up right where it left off.

This makes Temporal agnostic to the underlying binary — it doesn’t care about your code version as long as the timeline of inputs and outputs stays consistent. As long as your workflow is deterministic (same inputs → same outputs → same decisions), the replay will be identical. This is crucial for our devops requirement that a cron job simply couldn’t solve for.

Temporal also comes pre-built with a signaling system, allowing us to interact with running workflows really easily. We can signal all running temporal workflows that the user wishes to pause execution, or that the user updated a node's data. This solves our first functional requirement. 

Temporal timers solve for the second functional requirement as they allow us to sleep for an arbitrary amount of time, replaying that event history we talked about when the timer goes off. We had to wrap this using the signal system for a pausable timer. This interface is shown below where our timer can handle UI state changes.

type PausableTimer[T NodeData] interface {
	Update(ctx workflow.Context, node TypedNode[T])
	Pause(ctx workflow.Context)
	Resume(ctx workflow.Context)
	IsComplete() bool
	Complete(ctx workflow.Context)
	Listen(ctx workflow.Context, selector workflow.Selector)
}

We can now handle when the user updates the node, pauses the workflow, resumes etc. for different timer nodes. So now users and devs can change how this node works in real time.

A final functional requirement also leverages the signaling system. Workflows can include logic such as “wait until a contact’s record is updated,” and must resume once that change occurs. To support this, we’ve implemented an event notification mechanism that alerts all relevant workflows whenever an activity takes place. When notified, each workflow wakes up, evaluates whether its continuation condition is met, and, if not, returns to a waiting state until it is.

Hosting

Temporal is written in go with a nice SDK that plays nice with our backend. We host our own instance on a postgres database, which has been super easy to manage with the open source helm chart and has given us exactly the kind of scale we require. 

There are tons of limits and guardrails hidden all over Temporal but after finding these gotchas we have been very happy with this service in general. We deployed elastic search with custom search attributes for easy debugging and exposed the UI behind google authentication for easy access by our backend team. A key decision we made here was deploying a different worker for each task queue, so that if a task queue gets bogged down, other workers will continue to process. 

Drawbacks and Challenges

Temporal (and similar workflow engines) solves one of the hardest problems in distributed systems,  reliably managing long-lived processes, but it comes with its own set of challenges. The biggest hurdle is enforcing backward compatibility across workflow versions. A single non-deterministic code change or incompatible deployment could break every running workflow in production. We’ve built a rigorous history replay testing system to constantly sample and replay current histories to catch these issues early, but this area will demand even more infrastructure investment as we scale.

Another pain point is Temporal’s signal system, which isn’t designed for the high-throughput signaling we need. Signal rate limits make large-scale fan-out difficult, and the batch signal API proved too slow and limited in parallelism. To work around this, we use Kafka to stream signals efficiently, both for workflow triggers and CRM event propagation, while caching workflow configurations to determine when a signal actually needs to be sent. This ensures we stay accurate without overloading the system.

Looking ahead, we’ll face new scaling challenges: managing more task queues, namespaces, and workers will require full infrastructure-as-code automation. We’ll also need reliable recovery systems for failed workflows, and worker versioning to reduce risk during large updates. Most of our future optimization will happen at the event-sending layer: making signaling faster, more selective, and more reliable so that workflows start and progress exactly when and how they should.

If any of these challenges sound interesting to you, Conversion is hiring! We’re constantly working to build new features, improve reliability, and scalability. Please reach out if you’d like to join us on the mission of building the future of marketing.

Engineering

Service Framework: The Journey Behind Service to Service Communication at Conversion

October 6, 2025
00 Minute Read

Background

Conversion is building the marketing automation platform of the future. As we have continued to scale our product features, we have also scaled the amount of endpoints reaching over 350 managed endpoints across 10 services. This post will detail the journey we took with service to service communication used to handle >30 million requests a day.

With the introduction of generics, we’ve seen new frameworks like huma and fuego which handle endpoint routing and generating documentation. This blog post details our Service Framework which goes one step further and allows services to call each other without an intermediate representation (and thus build step).

OpenAPI Generated Handlers

Within the software industry debate there is a heavy debate between monolithic architectures and microservices. At Conversion, we believe that services should be “macroservices” which means for basic functions we call a function instead of making a network request but get the benefit of separation for large concepts to keep our codebase manageable. Although we try to reduce “service sprawl”, we’ve seen the business need to separate into a few macroservices due to scaling limitations, domain specification, and centralizing common resources.

Our first approach to service to service communication was to generate handlers based on the OpenAPI specification we had for each service. At this point in our journey, all of our functions were commented in Golang with the documentation that could generate OpenAPI specifications using swag. The natural step was to use a framework to generate client handlers based on this specification.

Press enter or click to view image in full sizeGodoc Example

We accomplished this with go-swagger which allows us to specify templates to generate files with Golang code based on the OpenAPI specification. While this served us well for over a year, our service complexity continued to increase and we saw shortcomings in the lack of strong typing, slow generation times (>2 mins), and poor developer experience requiring developers to regenerate handlers manually. Additionally, engineers needed to context switch between the terminal, repositories, and their IDE — disrupting the coding “flow” and creating a poor developer experience. We had clearly reached the limits of this setup and needed the next evolution.

Generic Schema and Handlers

For a while, there seemed to be no great answer to our problem without adopting heavy handed tools like protobuf / gRPC / bazel. However, we decided that for our scale (~7 engineers) these tools would create a large maintenance overhead and preferred simpler to maintain solutions.

Fortunately, in Go 1.18, Golang introduced generics which are a strong language primitive allowing logic and code to be re-used for different types. This, in combination with the fact that our backend is completely written in Golang and uses Fiber, allowed us to come up with a framework that defines schemas directly in Golang and use them in handlers without reflection or code generation.

At a high level, developers write a schema which defines the types that this endpoint uses. This schema is then used to instantiate a route which requires the handler function to match and accept the specified types. This guarantees via typing that the function is valid to represent this endpoint / schema. Finally, other services have all the metadata required to call this endpoint via the schema and can require a type safe request to be constructed all in Golang.

An example schema would be the following which defines a single endpoint that has a path with the templateId parameters:


package emailschema

type TemplatePathParams struct {
    TemplateId uuid.UUID `json:"templateId" validate:"required"`
}

var (
    TakeTemplateScreenshotV1 = schema.EndpointWithPath[TemplatePathParams, string, EmailApiHandler]{
        EndpointSchema: schema.NewEndpointSchema[EmailApiHandler](
            "TakeTemplateScreenshotV1",
            "/v1/templates/:templateId/screenshot",
            http.MethodPost,
        ),
    }
)


We can then define a type safe endpoint using the following pattern:

func (a *App) TakeTemplateScreenshotV1(ctx context.Context, params emailschema.TemplatePathParams) string {
  return "success"
}

routing.RouteEndpointWithPath(
  emailschema.TakeTemplateScreenshotV1, 
  endpointBuilder, 
  a.TakeTemplateScreenshotV1
)


Finally, we can call this endpoint from another service using a generic handler invocation:

resp, err := emailschema.TakeTemplateScreenshotV1.
  New(app.EmailHandler).
  WithPathParams(emailschema.TemplatePathParams{
    TemplateId: uuid.MustParse('UUID')
  }).
  Do(ctx)


All of this is made possible by our Service Framework! We now have a type safe service endpoint and type safe way to call other services. This is all done natively with generics greatly simplifying our build process (just go build!), improving developer experience with realtime feedback, and using an interface definition language (IDL) that must be correct.
The above example is a slightly abbreviated example of what it looks like to use our service framework. While we cannot open source our entire implementation due to its deep integration with our tech stack, we have created a simple version of this framework in an open source repository at https://github.com/tapp-ai/service-framework-example! This repository includes a simpler version of the machinery we use and a full example of an implementation.

The new Service Framework has been in production for over a month and we continue to migrate services to use the new framework. Beyond the many benefits including no build steps and greatly speeding up the developer experience, we’ve also been able to leverage this new framework to build infrastructure level tools like detailed observability, rate limiting, and request validation.

It’s an understatement to say that this has led to a big improvement in the intangible of “how fun it is to write code at Conversion” while delivering numerous business benefits.

Acknowledgements

The Service Framework is a collaboration and achievement shared by all of the backend engineering team at Conversion. This wouldn’t have been possible without the inspiration and leadership of Tayler who has been the internal champion of the new framework and led many of the early proof of concepts.

This achievement would also not have been possible without the help of Charlie, James, Naasir, and Swamik with their help in migrating our existing and critical endpoints to use the new framework.

If you love tackling interesting technical problems like the ones in this article, Conversion is hiring! We’re constantly working to build new features, improve reliability, and scalability. Please reach out if you’d like to join us on the mission of building the future of marketing.

Engineering

Behind the Curtain: How Conversion Syncs Millions of Customer Records a Day

October 6, 2025
00 Minute Read

Background

Conversion is building the marketing automation platform of the future. Powering AI augmented email nurture, a new standard for lead scoring, and integrated product data in their workflows is no small feat and requires solving difficult technical problems. This article dives into how we designed and built the data connector and data synchronization platform — the component responsible for syncing millions of our customers’ most important business records in from and out to their CRM’s every day.

Architectural Considerations

Our urgency for the initial version of the platform and first connector (Hubspot) was astounding — all initial pilots were ready to try our product whenever the first version was ready. At startups, the winners are decided by iteration speed and decision making so the pressure was on.

While it would be easy to lean too far into “build fast and break things”, we prioritized reliability and robustness of the system, while secondarily optimizing for synchronization latency. At Conversion, we understand our customers trust us with some of their most critical business workflows and data, and it could be disastrous if we write the wrong data back to customer CRM’s or miss running an automation for a set of high intent prospects in a campaign.

With this in mind, we designed a system with built in retries, reconciling capabilities, and almost real-time data syncs.

Data Modeling and Orchestration

Data models within different CRMs have complex relationships and dependencies. Our first challenge was transforming and combining high cardinality, heavily interdependent data that varied based on the CRM into our opinionated data philosophy of how you should model marketing customer lifecycle data. 

We opted with running our own in-cluster apache airflow instance. We essentially ran ETLs that would pull in, transform and load data from external connectors into our systems. This was lightweight, had built in system retries, and allowed us to schedule, reconcile, and debug specific nodes in the DAG’s without failing the entire run. This approach also allowed us to design data syncs around the dependencies of external providers — eg custom ordering of nodes based on the external providers entities. 

One unique aspect of our implementation is that our backend systems are all written in Golang! In order to make this work, we use K8s operators running go binaries instead of Python

How We Plug and Play Connectors

There are many sources of data that need to be synced into our systems (we haven’t even talked about data warehouses or our public API). It was top of mind to make it as easy as possible to integrate new connectors into our systems and maintain current ones! 

We leveraged Go’s interfaces to make an object hierarchy that abstracted away writing entire ETL scripts into implementing simple interfaces based on the external API’s. This works by making each DAG node instantiate an instance of the syncer configured with environment variables from that node. That syncer instance handles all generic ETL logic of parallelizing and looping through data. Finally, it calls interface functions that are CRM + external entity specific. 

The end goal with the platform is that if you want to sync another entity for a specific CRM, the core syncing logic is re-usable and the developer only has to implement a interface specific for the data source.

Currently Supported CRM Connectors

For Hubspot we currently sync the following entities: Contacts, Companies, Deals, Calls, Meetings, Notes, Tasks

For Salesforce we currently sync the following entities: Accounts, Contacts, Events, Leads, Notes, Tasks, Opportunities

The tricky part here is that each of these entities could have thousands of custom fields per instance of an entity. Storing such high cardinality data to be easily and efficiently queryable is slated for a future blog post!

Scheduling and Reconciling

It’s inevitable that the syncer will have downtime and our goal is to recover as fast as possible. In order to make this a reality, we have a robust system of scheduling and reconciling.

Diving a bit deeper into the syncer — when a customer installs the Conversion app into their CRM we kick off the inital syncing job. After this, we run a continuous sync every two minutes. Within each of these syncs, we break down the data that needs to be processed into small chunks (<1 MB) which is then stored in a Google cloud bucket.

Chunking the large amounts of incoming data has a two distinct advantages: parallel processing and never losing progress. Whether it is a transient failure, node restart, or bug in a later stage of the syncer, we are able to restart processing for a specific chunk in under 10 seconds helping us drive down the syncing latencies even when there are errors.

With chunking, we now do the following whenever we face an error:

  • Retry from the failed DAG node + chunk combination up to 3 times
  • Page on-call Conversion engineer if a specific chunk fails after retries
  • Reconcile all data from transient errors via a daily cronjob

Overall, this architecture has helped us drastically reduce the time to recovery and ensure customers don’t miss critical updates.

Looking Ahead

We hope this small peak behind the curtain at one of our most critical systems was insightful. We could write pages upon pages of implementation details but in the spirit of keeping the reading brief we’ve spared you guys many of the details that we’ve spent hundreds of hours contemplating!

Getting this far would’ve been impossible without the talented engineers at Conversion who maintain and continuously improve this living, breathing data synchronization system. Especially huge shoutouts to Swamik Lamichane and Naasir Farooqi for being main contributors on the project.

If you love tackling interesting technical problems like the ones in this article, Conversion is hiring! We’re constantly working to build new features, improve reliability, and scalability. Please reach out if you’d like to join us on the mission of building the future of marketing.

Turn Every Form Fill-Out Into Your Next Customer

Trigger personalized emails and actions based on real-time behavior, not static lists.