Add #ajax event to a custom composite element I have created a custom composite element for Webform (6.0) that has a date field and a select field. The idea is when a user picks a date, an API call is made that returns available time slots to make a booking.
I am familiar with how this is normally done in FormBase forms and inspecting form_state to inform build values, but I am stuck here.
class WebformTourAvailabilityComposite extends WebformCompositeBase {

  /**
   * {@inheritdoc}
   */
  public static function getCompositeElements(array $element) {
    $date_format_entity = DateFormat::load('html_date');
    $date_format = $date_format_entity-&gt;getPattern();

    $elements = [];

    $elements['tour_date'] = [
      '#type' =&gt; 'date',
      '#title' =&gt; t(&quot;Tour Date&quot;),
      '#date_date_format' =&gt; $date_format,
      '#date_year_range' =&gt; '0:15',
      '#attributes' =&gt; [
        'min' =&gt; '[webform_submission:created:html_date]',
        'onkeydown' =&gt; 'return false'
      ],
      #'#datepicker' =&gt; TRUE,
      '#datepicker_button' =&gt; TRUE,
      '#date_label_position' =&gt; 'invisible',
      '#required' =&gt; TRUE,
      '#ajax' =&gt; [
        'callback' =&gt; 'mymodule_get_date_options',
        'wrapper' =&gt; 'available-times'
      ]
    ];

    $elements['tour_time'] = [
      '#type' =&gt; 'select',
      '#title' =&gt; t('Tour time'),
      '#prefix' =&gt; '&lt;div id=&quot;available-times&quot;&gt;',
      '#suffix' =&gt; '&lt;/div&gt;',
      #'#options' =&gt; [],
    ];

    return $elements;
  }

}

in mymodule.module:
function mymodule_webform_submission_form_alter(&amp;$form, \Drupal\Core\Form\FormStateInterface $form_state) {
  $selected = $form_state-&gt;getValues();

  if (isset($selected[&quot;select_appointment&quot;][&quot;tour_date&quot;])) {
    $response = \Drupal::httpClient()-&gt;get('https://nestiolistings.com/api/v2/appointments/group/123/available-times');
    $options = json_decode($response-&gt;getBody()-&gt;getContents(), TRUE);
    #$form[&quot;elements&quot;][&quot;step_1&quot;][&quot;select_appointment&quot;][&quot;tour_time&quot;][&quot;#options&quot;] = $options['data'];
    $form[&quot;elements&quot;][&quot;step_1&quot;][&quot;select_appointment&quot;][&quot;#webform_composite_elements&quot;][&quot;tour_time&quot;][&quot;#options&quot;] = $options['data'];
  }
}

function mymodule_get_date_options(&amp;$form, \Drupal\Core\Form\FormStateInterface $form_state) {
  return $form;
}

Which method I should override or use in the Webform ecosystem, or if this needs to be a composite at all and I can do this some other way?
Turns out I was pretty close, an external module was causing interference while authenticated (was getting a post max size exceeded error when submitting the form) on the front end. Unrelated to Webform.
You can add ajax to composite elements and react to them in afterBuild like so (or in your own custom form elements, added to the composite):
class WebformTourAvailabilityComposite extends WebformCompositeBase {

  /**
   * {@inheritdoc}
   */
  public static function getCompositeElements(array $element) {
    $date_format_entity = DateFormat::load('html_date');
    $date_format = $date_format_entity-&gt;getPattern();

    $elements = [];

    $elements['tour_date'] = [
      '#type' =&gt; 'date',
      '#title' =&gt; t(&quot;Tour Date&quot;),
      '#date_date_format' =&gt; $date_format,
      '#date_year_range' =&gt; '0:15',
      '#attributes' =&gt; [
        'min' =&gt; '[webform_submission:created:html_date]',
        'onkeydown' =&gt; 'return false'
      ],
      '#datepicker_button' =&gt; TRUE,
      '#date_label_position' =&gt; 'invisible',
      '#required' =&gt; TRUE,
      '#ajax' =&gt; [
        'callback' =&gt; ['\Drupal\mymodule\Element', 'refreshTimes'],
        'wrapper' =&gt; 'available-times'
      ],
      '#prefix' =&gt; '&lt;div id=&quot;available-times&quot;&gt;',
    ];

    $elements['tour_time'] = [
      '#type' =&gt; 'select',
      '#title' =&gt; t('Tour time'),
      '#suffix' =&gt; '&lt;/div&gt;',
      '#after_build' =&gt; [[get_called_class(), 'afterBuild']],
      '#validated' =&gt; TRUE,
    ];

    return $elements;
  }

  public static function refreshTimes($form, FormStateInterface $form_state) {
    return $form['elements']['tour_time'];
  }

  /**
   * Performs the after_build callback.
   */
  public static function afterBuild(array $element, FormStateInterface $form_state) {
    $client = \Drupal::service('mymodule.client');
    $selected = $form_state-&gt;getValues();

    if (isset($selected['tour_date']) &amp;&amp; strlen($selected['tour_date'])) {
      $times = $client-&gt;getAvailableTourTimes($selected[&quot;tour_date&quot;]);
      $element['#options'] = count($times) ? $times : ['' =&gt; '- Select -'];
      $element['#description'] = count($times) ? '' : 'There were no available times found for the date you selected. Please choose another date.';
    }

    return $element;
  }

}

Note that your form keys will vary depending on your form, what you name fields, etc. On afterBuild, in this case, if the date picker has a value submitted, it will call a remote API and replace the options in the select with values.