"Majority" sounds simple until a real board asks: what if it is a tie? What if one member never votes? Can the chairman's vote be required? We answered these questions in Approv, a board approval software for boards and committees in Saudi Arabia. This post shows the model we ended with.
Four strategies, one number
Every committee has a voting strategy. Each strategy maps to one target: the approval percentage a request needs.
class Committee extends Model { const STRATEGY_UNANIMITY = 0; const STRATEGY_MAJORITY_TIE = 1; // a tie passes const STRATEGY_MAJORITY_NO_TIE = 2; // a tie fails const STRATEGY_ADVANCED = 3; // custom percentage public function targetPercentage(): int { return match ((int) $this->strategy) { self::STRATEGY_UNANIMITY => 100, self::STRATEGY_MAJORITY_TIE => 50, self::STRATEGY_MAJORITY_NO_TIE => 51, self::STRATEGY_ADVANCED => $this->max_percent ?: 100, }; } } The tie rule is a business decision, not a math detail. With 4 members and 2 approvals, the result is 50%. Under "majority, tie passes" (target 50, compared with >=), the request passes. Under "majority, tie fails" (target 51), it fails. Each committee picks one. We do not guess.
The advanced strategy covers bylaws such as "two thirds of the board". The admin enters a percentage between 10 and 100.
Who counts in the denominator
We compute the result as approvals divided by all voters on the request:
$percent = round($stats['approved'] / $stats['total'] * 100); Members who did not vote stay in the denominator. A board of 7 with 3 approvals and 4 silent members is at 43%, not 100%. This matches how most bylaws read: a decision needs the approval of a share of the members, not of the people who happened to reply.
There is no "abstain" button. A member votes yes or no. A member who does not vote before the request closes is marked skipped, and the log shows it.
Mandatory voters
Some approvals need a specific person, such as the chairman or the CFO, whatever the percentage. Each voter on a request has a mandatory flag. A request passes only when the target is met and every mandatory voter approved.
When a request closes
A request closes in one of three ways, and we record which one as the close trigger:
- VOTES: every voter has voted. The outcome is final.
- INSTANT_CLOSE: the committee allows early closing. As soon as the target is met and all mandatory voters approved, the request passes without waiting for the rest.
- MAX_DURATION: a deadline passed. A scheduled job checks open requests. If the target is met at that moment, the request passes; if not, it is rejected.
if ($stats['pending'] === 0) { $decision = ($percent >= $target && $mandatoryApproved) ? self::DECISION_APPROVED : self::DECISION_REJECTED; $this->markClosed($decision, trigger: 'VOTES'); } When a request closes with pending voters, those voters are set to skipped in the same step. Nobody can vote on a closed request.
Who voted, and how we know
A vote link alone is not proof of identity. Before a member votes, they confirm a one-time code sent by email or SMS. The vote record stores the method used, the address the code went to and the time. Every vote also adds a line to the request log, and an audit table records changes to voter rows.
When the request closes, the system builds a PDF of the request, its attachments and the votes, then signs it with a digital certificate. The PDF is the record the company keeps for its governance files.
Lessons
- Make the tie rule explicit per committee. Boards disagree on it.
- Decide early who counts in the denominator, and write it in the help text.
- Store why a request closed, not only that it closed.
- Required people are a separate rule from percentages.
ملخص بالعربية: يشرح هذا المقال كيف يحسب نظام إدارة اعتمادات مجالس الإدارات نتيجة التصويت لكل لجنة. ويتناول الإجماع والأغلبية وحالة التعادل والنسبة المخصصة، والأعضاء الذين يشترط النظام موافقتهم. كما يوضح متى يغلق الطلب، وكيف يوثق كل صوت قبل اعتماده.
Written by the engineering team at Renewable Systems in Riyadh, the makers of Approv.