Shareholder Proxy Voting, Automated in Two Flows
An asset manager's proxy voting ran on manual handling of vendor meeting data. We rebuilt it as two automated flows — US equities first, proven in production, then global (non-US) meetings on the same proven pipeline — both now running end-to-end.
The Problem
An asset manager that holds equities on behalf of its funds carries an obligation that does not go away: it has to vote at the shareholder meetings of the companies it holds, and it has to keep a defensible record of how it voted. The work is unglamorous and entirely deadline-driven. A meeting has a date; a ballot has a cut-off; a vote that misses its window cannot be re-cast. The cost of getting it wrong is not proportional to the effort of the step that was missed.
The raw material for all of this is meeting data — agendas, resolutions, ballot items, deadlines — supplied by a third-party proxy data provider. Before this engagement, that data was handled largely by hand: pulled from the vendor, reconciled against holdings, turned into voting instructions, and recorded. It worked, but it was brittle in exactly the way manual data handling always is. Every cycle depended on a person doing the same careful sequence without slipping, and the volume only ever grew.
The deeper problem was that "proxy voting" is not one population of meetings — it is two, with very different shapes. US-listed equities are a single, well-understood market: consistent ballot conventions, predictable timing, a familiar disclosure regime. Global meetings — everything that is not US — are a sprawl of markets, each with its own ballot formats, languages, timing conventions, and data-quality quirks. The manual process treated both as the same kind of work, and the heterogeneity of the global side was where the brittleness concentrated.
The goal of the engagement was to replace the manual handling with an automated pipeline that ingests meeting data, matches each meeting to the funds' holdings, applies the voting policy, and records the outcome — reliably, every cycle, with an audit trail. The constraint that shaped the whole design was that this could not be switched on as a big bang across both populations at once.
The Constraints
- US and global meetings are structurally different. Ballot formats, market conventions, timing, and data quality vary enough that a single undifferentiated flow would have been fragile. The design had to separate the two populations rather than pretend they were one.
- Meeting deadlines are absolute. A vote that misses its meeting is gone. The pipeline had to be reliable against fixed external dates, not merely fast on average.
- Production proof before expansion. The US flow had to run against real meetings in production and be shown correct before the larger, messier global universe was brought onto the same pipeline.
- Vendor data is the single source — and it varies. All meeting data arrives from a third-party provider. Ingestion had to tolerate format variance and incomplete records without silently dropping a meeting.
- Every vote must be auditable. The holdings it was matched against, the policy that was applied, and the instruction that was produced all had to be recorded so that any vote can be reconstructed after the fact.
The Architecture
The system is two flows over one shared pipeline. Both the US-equities flow and the global-meetings flow follow the same four stages — ingest meeting data, match it to holdings, apply the voting policy, record and submit — but each flow owns the market-specific behaviour at every stage. The shared pipeline owns scheduling, persistence, and the audit trail, so the reliability work is written once and used by both.
Keeping the flows separate but the pipeline shared was the central decision. A combined flow would have forced US voting — the stable, high-volume case — to absorb every quirk of every non-US market in the same code path. By giving each flow its own ingest, match, vote, and record behaviour while sharing the scheduling, storage, and audit machinery underneath, we could ship the US case, run it against real meetings, and only then take on the heterogeneity of global markets, with the reliability layer already battle-tested.
Implementation Highlights
One Pipeline, Two Flows
Both populations of meetings are processed through the same four stages, expressed as a single interface. The US and global flows are separate implementations of that interface, so the shared scheduler and audit layer treat them identically while each flow keeps its market-specific logic to itself. Adding a future market means adding an implementation — nothing in the shared pipeline has to change.
public interface IVotingFlow
{
/// Stable identifier used in scheduling and audit partitioning.
string FlowCode { get; }
/// Pull meeting and ballot data from the proxy-data vendor.
Task<IReadOnlyList<Meeting>> IngestAsync(CancellationToken ct);
/// Match each meeting to the funds' current holdings.
Task<IReadOnlyList<VotableMeeting>> MatchAsync(
IReadOnlyList<Meeting> meetings, CancellationToken ct);
/// Apply the voting policy and record + submit each instruction.
Task<VotingResult> VoteAsync(
IReadOnlyList<VotableMeeting> meetings, CancellationToken ct);
}
// US-listed equities — a single market with consistent ballot conventions.
public sealed class UsEquitiesFlow : IVotingFlow
{
public string FlowCode => "US-EQUITIES";
// ... ingest / match / vote tuned for the US market
}
// Non-US meetings — many markets, varied ballot formats and timing.
public sealed class GlobalMeetingsFlow : IVotingFlow
{
public string FlowCode => "GLOBAL-MEETINGS";
// ... per-market adapters for ballot variance and timing
}
Shipping One Flow at a Time
The phased rollout was enforced in configuration rather than by branching code. The scheduler runs only the flows that are switched on, so the global flow could sit fully built but dormant while the US flow proved itself in production — and could be enabled with a configuration change rather than a redeploy once it was ready.
public sealed class VotingFlowOptions
{
public bool UsEquitiesEnabled { get; init; } = true;
// Off until the US flow was proven in production.
public bool GlobalMeetingsEnabled { get; init; } = false;
}
public sealed class VotingScheduler
{
private readonly IReadOnlyList<IVotingFlow> _flows;
private readonly VotingFlowOptions _options;
public async Task RunDueFlowsAsync(CancellationToken ct)
{
foreach (var flow in _flows)
{
if (!IsEnabled(flow.FlowCode)) continue;
var meetings = await flow.IngestAsync(ct);
var votable = await flow.MatchAsync(meetings, ct);
await flow.VoteAsync(votable, ct);
}
}
private bool IsEnabled(string code) => code switch
{
"US-EQUITIES" => _options.UsEquitiesEnabled,
"GLOBAL-MEETINGS" => _options.GlobalMeetingsEnabled,
_ => false
};
}
The Outcome
The brittle, manual handling of vendor meeting data was replaced by an automated pipeline that ingests the data, matches it to holdings, applies the voting policy, and records every vote with its supporting data. The US-equities flow went first and was confirmed correct against live meetings; only then was the global-meetings flow brought onto the same pipeline. Both are now running correctly in production.
Just as important as the automation itself was the shape of the delivery. Sequencing US before global meant the riskier, more varied population was tackled on a pipeline whose scheduling, persistence, and audit behaviour had already been exercised in production — so the second flow inherited a proven foundation rather than discovering its rough edges live.
What We'd Do Differently
The two-flow separation was the right call and we would make it again — a unified flow would have forced the stable US case to carry every non-US quirk in the same code path. What we would change is when we drew the line between "shared pipeline" and "flow-specific" code. We factored the shared scheduling, persistence, and audit layer cleanly, but some matching and ingestion logic that turned out to be common across markets was first written inside the US flow and later lifted out. Pulling that shared core out earlier would have made the global flow cheaper to build.
On the global side specifically, the per-market variance — ballot formats, timing conventions, data-quality gaps — is the part that keeps costing time as new markets appear. A thin, explicit per-market adapter contract from day one, rather than handling variance case by case as it surfaced, would have made each new market a small, well-bounded addition instead of a fresh round of special-casing.
If You're Solving This Today
The pattern that mattered here was not any single technology — it was sequencing a risky rollout so that the hard case lands on a foundation that has already proven itself in production. If you are automating a deadline-bound, vendor-fed workflow that spans a clean core case and a messy long tail, build the core case first, run it live, and keep the long tail behind a switch until the shared machinery underneath it is boring. The same instinct shows up in our 1099 reporting automation, where a hard regulatory deadline made a controlled, staged rollout the safe choice.
Related Case Studies
Questions about proxy voting automation.
- Why was proxy voting automated as two separate flows?
- US-listed equities and global (non-US) meetings differ structurally — ballot formats, market conventions, deadlines, and data quality vary by market. One undifferentiated flow would have let the more heterogeneous global universe destabilise the US path. Separating them let us prove the US flow in production first, then add global meetings on the same proven pipeline pattern.
- Why ship the US flow before the global one?
- Proxy voting has hard meeting deadlines, and a vote that misses its meeting cannot be re-cast. Shipping the simpler single-market US flow first let us validate ingestion, holdings matching, policy application, and record-keeping against real meetings before extending the same pattern to the larger and more varied global universe.
- Where does the meeting data come from?
- Meeting and ballot data is ingested from a third-party proxy data provider. The automated pipeline collects that data, matches each meeting to the funds' holdings, applies the voting policy, and records every vote with its supporting data so any vote can be reconstructed for compliance.
A Deadline-Bound Workflow Still Running by Hand?
The fastest way to scope this safely is a two-week Discovery Sprint — a fixed-price diagnostic that maps the workflow, the integration points, and the risks before you commit to anything.