Almost there. I have the ahah appearing now by using #after_build. I guess from what I read that this had something to do with CCK fields (such as content profile fields) not cooperating with hook_form_alter due to timing. My (hopefully) last hurdle is probably just a need for another pair of eyes. When the change event fires and the throbber spins, I get a 404 error on the callback. function test_menu() { $items = array(); $items['test/change_value/callback'] = array( 'page callback' => 'test_change_callback', 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); return $items; } function test_form_alter(&$form, &$form_state, $form_id) { if ($form_id == 'test_profile_node_form') { $form['field_result']['#prefix'] = '<div id="field-result-wrapper">'; $form['field_result']['#suffix'] = '</div>'; $form['#after_build'][] = 'test_add_ahah'; } } function test_add_ahah($form, $form_state) { $ahah = array(); $ahah['edit-field-test-value'] = array( 'button' => false, 'effect' => 'none', 'event' => 'change', 'keypress' => NULL, 'method' => 'replace', 'progress' => array('type' => 'throbber'), 'selector' => '#edit-field-test-value', 'url' => 'test/change_value/callback', 'wrapper' => 'field-result-wrapper', ); drupal_add_js(array('ahah' => $ahah), 'setting'); return $form; } function test_callback_helper() { $form_state = array('storage' => NULL, 'submitted' => FALSE); $form_build_id = $_POST['form_build_id']; $form = form_get_cache($form_build_id, $form_state); $args = $form['#parameters']; $form_id = array_shift($args); $form_state['post'] = $form['#post'] = $_POST; $form_state['ahah_submission'] = TRUE; $form['#programmed'] = $form['#redirect'] = FALSE; drupal_process_form($form_id, $form, $form_state); $form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id); return $form; } function test_change_callback() { $form = test_callback_helper(); $output= test_do_stuff(); drupal_json(array('status' => TRUE, 'data' => $output)); exit(); }