Skip to content
Gravity Tables
tutorial

Build an event registration and attendee dashboard with Gravity Forms

A complete pattern for running an event off Gravity Forms: public registration form, organiser triage workspace, on-the-door check-in tablet view, public attendee directory, and post-event reporting. One form, four URLs, no Eventbrite fee.

· 8 min read · By Fahd Murtaza

Eventbrite charges 3.7% + $1.79 per ticket. For a 200-person conference at $400 a seat, that’s nearly $4,000 in fees you keep if you handle registration on your own WordPress install. Plus you own the data, the email list, and the post-event report.

This guide ships a complete event-management workflow on one Gravity Form: registration intake, organiser triage, on-the-door check-in, public attendee list, and per-session reporting.

What you’ll build#

Four URLs, all driven by a single Gravity Form:

  • /register, public registration form
  • /staff/registrations, organiser dashboard (the inbox; bulk approve, reassign, export attendee list)
  • /door, tablet-friendly check-in view used at the venue
  • /attendees, public list of confirmed attendees (opt-in)

Plus a manager view at /staff/reports for session-by-session capacity and revenue summaries.

Step 1: shape the registration form#

Customer-facing fields:

  • first_name, last_name, email, required
  • company, job_title, optional, but useful for the public attendee list
  • session_choice, single-select with all session names, or multi-select for multi-track events
  • dietary_restrictions, paragraph (catering planning)
  • payment_token, only if you’re charging via Stripe / PayPal / etc.

Hidden admin-side fields:

  • status, Hidden, default pending_payment if paid, confirmed if free. Values: pending_payment, confirmed, checked_in, cancelled, refunded, waitlisted.
  • ticket_code, Hidden, populated by submission hook with a unique 8-char code (used for door scanning).
  • checkin_time, Date/time, default empty. Set when checked in at the door.
  • assigned_seat, Hidden, default empty. For events with assigned seating.
  • internal_notes, Paragraph, default empty. Organiser commentary.

Step 2: generate ticket codes on submission#

A small hook in your child theme’s functions.php:

add_action('gform_after_submission_event', function ($entry, $form) {
    $code = strtoupper(wp_generate_password(8, false));
    GFAPI::update_entry_field($entry['id'], 'ticket_code', $code);

    if (!rgar($entry, 'payment_token')) {
        GFAPI::update_entry_field($entry['id'], 'status', 'confirmed');
    }
}, 10, 2);

The 8-char code is what attendees show at the door. It’s short enough to be QR-encoded for badge printing, unique enough that collision is statistically negligible (62^8 ≈ 218 trillion combinations).

For paid events, leave status as pending_payment until your payment hook flips it to confirmed after a successful charge.

Step 3: build the public registration page#

Standard Gravity Forms shortcode at /register:

[gravityform id="event" title="false" description="false" ajax="true"]

After submission, redirect to a thank-you page that displays the ticket code and a calendar .ics download. Configure under Form Settings → Confirmations.

Step 4: build the organiser dashboard#

The triage workspace at /staff/registrations:

[gravity_table id="event"
  columns="created,first_name,last_name,email,session_choice,status,ticket_code,checkin_time,dietary_restrictions"
  allowed_roles="event-organiser,event-staff,administrator"
  allow_edit="status,assigned_seat,internal_notes"
  edit_permissions="status:event-organiser"
  filters="status,session_choice,created"
  bulk="confirm,cancel,export,email_ticket"
  bulk_permissions="cancel:event-organiser,email_ticket:event-organiser"
  audit_log="true"
  auto_refresh="true"
  refresh_interval="30"
  sort="created:desc"
  per_page="50"]

Six knobs doing real work:

  • filter_user_owns is omitted, staff see all registrations, not just their own (events are team-managed)
  • status editable by event-organiser only, staff can update internal notes and seat assignments, but only organisers move state
  • bulk includes email_ticket, custom bulk action that resends the confirmation email + ticket code (registered via the hooks doc)
  • auto_refresh every 30s, last-minute registrations appear in the dashboard automatically
  • audit_log, every state change is logged; useful when reconciling with payment processor exports

Saved view URLs#

A small navigation strip:

<nav aria-label="Registration views">
  <a href="/staff/registrations">All</a>
  <a href="/staff/registrations?gt_filter_status=pending_payment">Awaiting payment</a>
  <a href="/staff/registrations?gt_filter_status=confirmed">Confirmed</a>
  <a href="/staff/registrations?gt_filter_status=checked_in">Checked in today</a>
  <a href="/staff/registrations?gt_filter_status=cancelled,refunded">Cancellations</a>
</nav>

Step 5: build the door check-in view#

The on-the-door tablet view at /door looks different from the staff dashboard, it’s optimised for fast lookup and one-tap actions.

[gravity_table id="event"
  columns="ticket_code,first_name,last_name,session_choice,status"
  filter="status:confirmed,checked_in"
  allowed_roles="event-staff,event-organiser,administrator"
  allow_edit="status"
  edit_permissions=""
  bulk=""
  search_field="ticket_code,first_name,last_name,email"
  per_page="100"
  mobile_layout="cards"]

Notable choices:

  • filter includes both confirmed AND checked_in, staff can see who’s already through, useful for “have you seen X come in yet?” lookups
  • search_field restricts the global search to the four columns most useful at the door, staff type a partial name or paste a ticket code, get results in real time
  • allow_edit="status" with no role gate, anyone with door access can flip a row from confirmed to checked_in. The intentionally narrow column scope means they can’t edit anything else.
  • mobile_layout="cards", the tablet sees one card per attendee with a big touch-target for the status dropdown
  • bulk="", no bulk actions on the door view; check-ins are individual events

Auto-stamp check-in time#

When status flips to checked_in, automatically stamp the time:

add_action('gravity_tables_entry_updated', function ($entry_id, $form_id, $old, $new) {
    if ($form_id !== 'event') return;
    if (rgar($new, 'status') === 'checked_in' && rgar($old, 'status') !== 'checked_in') {
        GFAPI::update_entry_field($entry_id, 'checkin_time', wp_date('Y-m-d H:i:s'));
    }
}, 10, 4);

The dashboard’s checkin_time column then shows when each attendee arrived, useful for measuring arrival flow and identifying late-comers.

Step 6: build the public attendee directory#

For events that benefit from an “I see Sarah from Acme is going” public list (most B2B conferences do), an opt-in view at /attendees:

[gravity_table id="event"
  columns="first_name,last_name,company,job_title"
  filter="status:confirmed,checked_in"
  user_filter_field="public_listing:yes"
  allow_edit=""
  filters="company"
  per_page="50"
  sort="last_name:asc"
  mobile_layout="cards"]

Two important config bits:

  • user_filter_field="public_listing:yes", only attendees who explicitly opted into a “list me publicly” checkbox on registration appear. GDPR-defensible by default.
  • No email or any contact info in the columns list, the public directory shows only the data attendees explicitly agreed to share.

For paid events, this directory becomes a marketing asset: prospective registrants see who’s already coming, social proof drives more registrations.

Step 7: register the custom bulk actions#

The confirm, cancel, and email_ticket bulk actions need registration. Pattern matches other guides:

add_filter('gt_register_bulk_actions', function ($actions) {
    $actions['confirm'] = [
        'label' => 'Confirm selected',
        'capability' => 'edit_others_posts',
        'callback' => function ($entry_ids) {
            foreach ($entry_ids as $id) {
                GFAPI::update_entry_field($id, 'status', 'confirmed');
            }
            return ['success' => count($entry_ids), 'message' => count($entry_ids) . ' confirmed'];
        },
    ];

    $actions['email_ticket'] = [
        'label' => 'Resend ticket email',
        'capability' => 'edit_others_posts',
        'callback' => function ($entry_ids) {
            $sent = 0;
            foreach ($entry_ids as $id) {
                $entry = GFAPI::get_entry($id);
                $code = rgar($entry, 'ticket_code');
                $email = rgar($entry, 'email');
                if (!$email || !$code) continue;
                wp_mail(
                    $email,
                    'Your event ticket',
                    "Your code: {$code}\n\nDoors open at 9 AM."
                );
                $sent++;
            }
            return ['success' => $sent, 'message' => "Re-sent {$sent} tickets"];
        },
    ];

    $actions['cancel'] = [
        'label' => 'Cancel registration',
        'capability' => 'edit_others_posts',
        'prompt' => [
            'type' => 'textarea',
            'label' => 'Cancellation reason (optional, recorded in notes):',
        ],
        'callback' => function ($entry_ids, $args) {
            foreach ($entry_ids as $id) {
                GFAPI::update_entry_field($id, 'status', 'cancelled');
                if ($args['textarea'] ?? false) {
                    $existing = gform_get_meta($id, 'internal_notes');
                    GFAPI::update_entry_field($id, 'internal_notes', trim($existing . "\n" . wp_date('Y-m-d') . ": " . $args['textarea']));
                }
            }
            return ['success' => count($entry_ids), 'message' => count($entry_ids) . ' cancelled'];
        },
    ];

    return $actions;
});

Step 8: capacity reporting#

For events with capped sessions, organisers need real-time capacity by session. A separate [gravity_table] shortcode at /staff/reports with group_by:

[gravity_table id="event"
  columns="session_choice,_count,_sum_value"
  group_by="session_choice"
  totals="_count,_sum_value"
  filter="status:confirmed,checked_in"
  allowed_roles="event-organiser,administrator"
  allow_edit=""
  per_page="50"]

The grouped view gives one row per session with attendee count and revenue (if value is the ticket price). Compare to your venue’s capacity per session and fire a Slack alert when any session crosses 90%:

add_action('gravity_tables_entry_created', function ($entry_id, $form_id) {
    if ($form_id !== 'event') return;
    $entry = GFAPI::get_entry($entry_id);
    $session = rgar($entry, 'session_choice');
    if (!$session) return;

    $session_total = count(GFAPI::get_entries('event', [
        'field_filters' => [
            ['key' => 'session_choice', 'value' => $session],
            ['key' => 'status', 'value' => ['confirmed','checked_in'], 'operator' => 'in'],
        ],
    ]));

    $cap = SESSION_CAPS[$session] ?? PHP_INT_MAX;
    if ($session_total === intval($cap * 0.9)) {
        wp_remote_post(SLACK_WEBHOOK_URL, [
            'body' => json_encode(['text' => "⚠ {$session} is 90% full ({$session_total}/{$cap})"]),
        ]);
    }
}, 10, 2);

The integer-equality check prevents repeat alerts, the message fires once when the threshold is crossed, not on every subsequent registration.

What this gives you#

For a 50-500 person event, this configuration delivers:

  • No platform fees, replace 3.7% + $1.79 per ticket with your existing WP hosting cost
  • You own the data, attendee list, email addresses, dietary preferences all in your DB, no export job required
  • Real-time capacity tracking, organisers always know where they are vs. cap
  • Door experience that scales, staff with tablets check in 200 attendees in 20 minutes
  • Public attendee directory for social-proof marketing
  • Audit trail for refund disputes (“when was this registration cancelled?”)

What it doesn’t give you#

  • Payment processing (use Gravity Forms Stripe / PayPal add-ons; this guide assumes payment is wired separately)
  • Email broadcasting / drip sequences (use FluentCRM, Brevo, or Mailchimp; integrate via the lifecycle hooks)
  • Mobile app for attendees (web-based check-in via /door is the alternative)
  • Multi-event support (one form = one event; for recurring events, duplicate the form)

Recipe variations#

Free meetup or community event#

Drop payment_token, value, and the 90%-capacity Slack alert. Default status to confirmed on submission. The waitlist pattern still applies if you have venue caps.

Multi-track conference#

Promote session_choice from single-select to multi-select. Update the capacity report to group_by="session_choice" with attendees-per-session. Add a track_lead field for assigning each session to its organiser.

Workshop or training course#

Add a cohort field (e.g. “March 2026 Cohort”). Filter the public attendee list by cohort, so each cohort sees only their peers.

Internal company event (no public list)#

Drop /attendees entirely. Set allowed_roles="employee" on /staff/registrations. The internal team accesses the registration data via WP login.

Don’ts#

Don’t expose ticket_code or status on the public registration form#

Both are workflow primitives. A submitter setting their own status: confirmed skips your payment validation entirely. Always Hidden, always populated by hooks.

Don’t skip the public-listing opt-in for the attendee directory#

Posting names + companies without explicit consent fails GDPR. The public_listing:yes filter is non-negotiable for an EU-attended event.

Don’t run the door check-in on a flaky WiFi connection#

The check-in view requires a working connection. For venues with bad connectivity, pre-print a paper backup attendee list (export from /staff/registrations) and reconcile manually post-event.